Oprydning i tekst

Jeppe Fjeldgaard Qvist

2025-10-07

Dagens program

  • NLP (natural language processing)
  • SML vs. UML
  • Udfordringer ved tekst som data
  • Tekst som tal
    • Tekst som vektorer
    • Tekster som matrix
    • Tekster som ordtabel
    • Tekster som graf/netværk
  • Overvejeser når vi arbejder med tekst som data
  • Fra tekst til datastruktur
    • Bag-of-words repræsentation
    • Tf-idf vægtning
  • Sprogmodeller og SpaCy
  • Live-kodning

Læringsmål

  • Have kendskab til centrale koncepter inden for “computationel tekstanalyse”.
  • Forståelse for generelle muligheder med computationel tekstanalyse og NLP.
  • Forståelse for væsentlige udfordringer i at anvende computationel tekstanalyse til samfundsvidenskabelige analyser.
  • Forståelse for teknikker anvendt i NLP.
  • Kompetence til at anvende eksisterende sprogmodeller i Python.

Nye muligheder med (tekst)data

  • Øget tilgængelighed af eksisterende tekstdata (arkiver, nyhedsmedier, policy dokumenter)
  • Øget mængde af “digitalt fødte” data (sociale medier, kommentarspor, anmeldelser, reaktioner)

Nye muligheder født “nye” metoder

  • Bedre til at udnytte at data er digitale
  • Tilgængelighed af hardware tillader bearbejdning af større mængder af data
  • Øget genbrug af tilgængelige ressourcer (sprogmodeller, dictionaries, trænede modeller og classifiers)

Natural Language Processing (NLP)

Computervidenskabelig disciplin, der beskæftiger sig med, hvordan en computer kan forstå og producere menneskeligt sprog.

  • Behandling og bearbejdning af “naturligt sprog” ved hjælp af computerteknologi
  • Teknikker der involverer statistiske metoder til at forstå tekst; med eller uden lingvistiske indsigter
  • Krydsfelt mellem datalogi og lingvistik
  • Involverer i stigende grad brug af maskinlæringsteknologi
    • Eksempler fra hverdagen: ChatGPT, Tale-til-tekst applikationer, tekstforslag i beskeder, autokorrektur, oversættelsestjenester (Google Translate)

To profiler i feltet

(Maskinlærings)ingeniøren

  • Ofte interesseret i målbare koncepter
  • Forventer valideringsdata
  • Interesseret i generisk kontekst (genanvendelighed)

Samfundsforskeren

  • Ofte interesseret i latente koncepter
  • Må acceptere mangel på valideringsdata
  • Interesseret i specifik kontekst

Analyseformål

  • Skabe overblik (fx nøgleordsanalyse)
  • Identificér og måle prædefinerede koncepter (hate-speech, politisk ideologi, diskurs)
  • Udforske og forstå komplekse meningssammenhænge (hvordan temaer opstår og udvikler sig, hvordan befolkningsgrupper, institutioner eller andet italesættes, koblinger mellem temaer, holdning og mening)
  • Identificér sociale aktører og deres (formodede) handlinger (hvem gjorde hvad til hvem?)
  • Computationel analyse af sætningskonstruktion

Superviseret vs. Usuperviseret Machine Learning

Computationel tekstanalyse handler om at bruge computerens kapacitet til at finde mønstre, strukturer og betydninger i store mængder tekst, som ville være umulige for mennesker at behandle manuelt.


Supervised

  • Prædefineret ordliste(r) relateret til koncepter
  • Tekster (hele tekster, afsnit, sætninger) der er kategoriseret i forvejen (hvilket koncept afspejler teksten?)
  • Preprocessing evalueres via accuracy/F1-score

Unsupervised

  • Klyngeanalyser; Topic models
  • Tekst som vektorer (fx word embeddings)
  • Ingen klar/entydig metrik; fokus på interpretérbarhed

Superviserede metoder: Læring med vejledning

Forestil dig, at du skal lære et barn at genkende forskellige dyrearter. Den mest oplagte måde ville være at vise barnet billeder af dyr, hvor du hver gang siger: “Det her er en hund”, “Det her er en kat”, osv. Efter at have set mange eksempler kan barnet begynde at genkende nye dyr, det ikke har set før.

  • I superviseret læring giver vi computeren et træningsdatasæt, hvor hver tekst allerede er blevet mærket eller kategoriseret af mennesker.
  • Hvis vi for eksempel vil bygge et system til at klassificere filmanmeldelser som positive eller negative, starter vi med at give computeren tusindvis af anmeldelser, hvor vi på forhånd har markeret hver enkelt som enten positiv eller negativ. Computeren lærer så at finde de sproglige mønstre, der karakteriserer hver kategori.
  • De mest almindelige superviserede metoder til tekstanalyse omfatter klassifikationsalgoritmer som Naive Bayes, Support Vector Machines og neurale netværk.
  • En vigtig pointe er, at kvaliteten af den superviserede model er dybt afhængig af kvaliteten og omfanget af de “labeled” data, vi træner den med.

Naive Bayes klassifikation: læring gennem sandsynlighedsteori

Forestil jer, at du står med en filmanmeldelse og vil afgøre, om den er positiv eller negativ. Naive Bayes spørger: “Givet de ord, jeg ser i denne anmeldelse, hvad er sandsynligheden for, at den tilhører hver kategori?”

Dette tager afsæt i Bayes’ Theorem/-sætning:

\[ P(C \mid D) = \frac{P(D \mid C) \times P(C)}{P(D)} \]

Sætningen fortæller os, hvordan vi kan vende en betinget sandsynlighed om. Hvis vi kalder vores kategorier for \(C\) (“positiv” eller “negativ”) og vores dokument for \(D\) (repræsenteret ved de ord, det indeholder), så siger Bayes’ sætning:

  1. \(P(C \mid D)\) er den posterior sandsynlighed: sandsynligheden for, at dokumentet tilhører kategori C, givet at vi har observeret ordene i D.Det er altså hvad vi vil finde ud af.
  2. \(P(D \mid C)\) er vores likelihood: sandsynligheden for at se netop denne kombination af ord, hvis vi ved, at dokumentet tilhører kategori \(C\).
  3. \(P(C)\) er prior sandsynligheden: vores forhåndsforventning om, hvor sandsynlig kategorien er, før vi har set dokumentet. \(P(D)\) er en normaliseringskonstant, der sikrer, at sandsynlighederne summerer til én.

\[ P(C \mid D) = \frac{P(D \mid C) \times P(C)}{P(D)} \]

Den naive antagelse er at vi går ud fra uafhængighed mellem ord. I virkeligheden er ord i en tekst åbenlyst ikke uafhængige af hinanden.

Naive Bayes antager, at alle ord er betingelsesvist uafhængige givet kategorien, udtrykt som:

\[ P(D \mid C) = P(w_1, w_2, \ldots, w_n \mid C) = P(w_1 \mid C) \times P(w_2 \mid C) \times \cdots \times P(w_n \mid C) \]

hvor \(w_1, w_2, \ldots, w_n\) er de individuelle ord i dokumentet. Denne antagelse er objektivt forkert, men den fungerer stadig i praksis, fordi den gør beregningerne håndterbare.

Naive Bayes som Sentimentanalyse

Forstil jer at vi har trænet en model på tusindvis af filmanmeldelser, og vi ser denne korte anmeldelse: "Fantastisk film. Elsker!".

  1. Vi har to kategorier: positiv (\(P\)) og negativ (\(N\)).
  2. Fra vores træningsdata har vi lært følgende sandsynligheder.
    • Prior sandsynlighederne er \(P(P) = 0.6\) og \(P(N) = 0.4\): fordi \(60\) procent af vores træningsanmeldelser var positive.
    • Hvor ofte hvert ord optræder i hver kategori:
      • \(P(\text{fantastisk}|P) = 0.08\) (optrådte i 8 procent af positive anmeldelser) og \(P(\text{fantastisk}|N) = 0.01\) (kun 1 procent negative anmeldelser).
      • \(P(\text{film}|P) = 0.15\) og \(P(\text{film}|N) = 0.12\)
      • \(P(\text{elsker}|P) = 0.09\) og \(P(\text{elsker}|N) = 0.02\)
  1. Nu kan vi beregne den unormaliserede posterior sandsynlighed for positiv kategori:
    • \(P(P \mid D) \propto P(P) \times P(\text{fantastisk} \mid P) \times P(\text{film} \mid P) \times P(\text{elsker} \mid P)\)
    • \(P(P \mid D) \propto 0.6 \times 0.08 \times 0.15 \times 0.09 = 0.0006480\)
  2. Tilsvarende for negativ kategori:
    • \(P(N \mid D) \propto P(N) \times P(\text{fantastisk} \mid N) \times P(\text{film} \mid N) \times P(\text{elsker} \mid N)\)
    • \(P(N \mid D) \propto 0.4 \times 0.01 \times 0.12 \times 0.02 = 0.0000096\)
  3. Selv uden at normalisere kan vi se, at den positive sandsynlighed er omkring \(67\) gange højere end den negative. Efter normalisering ville \(P(P|D)\) være cirka \(0.985\), så modellen er meget sikker på, at dette er en positiv anmeldelse.

Live eksempel på Naive Bayes

Usuperviserede metoder: Selvstændig opdagelse

Nu forestiller dig i stedet, at du giver barnet en stor kasse med forskellige legetøjsting uden at fortælle noget om dem. Barnet vil naturligt begynde at sortere tingene i bunker baseret på ligheder, det selv opdager: det kunne fx lægger alle røde ting sammen, eller alle ting med hjul. Barnet opdager strukturer uden vejledning.

  • I usuperviserede metoder giver vi computeren tekster uden forhåndsmærkninger.
  • Vi “beder” smaskinen om selv at opdage mønstre, grupperinger eller strukturer i materialet. Computeren kigger på, hvilke ord der optræder sammen, hvilke dokumenter der ligner hinanden, eller hvilke latente temaer der går igen i samlingen af tekster.
  • Typiske usuperviserede metoder inkluderer topic modeling som Latent Dirichlet Allocation, hvor computeren identificerer skjulte temaer på tværs af dokumenter, clustering-algoritmer som \(k\)-means, der grupperer lignende dokumenter sammen, og word embeddings som Word2Vec, der lærer at repræsentere ord baseret på deres kontekstuelle anvendelse.

\(K\)-means clustering: geometrisk optimering

Hvor Naive Bayes arbejder med sandsynligheder og kræver labelet data, virker \(k\)-means clustering fundamentalt anderledes.

Vi skal forestille os at vi har tusindvis af dokumenter spredt ud i et højdimensionalt rum, hvor hver dimension repræsenterer et ord eller en tekstfunktion. \(K\)-means forsøger at finde naturlige grupperinger i dette rum ved at minimere afstanden mellem dokumenter i samme gruppe.

Geometriske fundament: Afstand og centrum:

1: Hvert dokument er repræsenteret som en vektor i et højdimensionalt rum. Hvis vi bruger en bag-of-words repræsentation med \(N\) unikke ord, bliver hvert dokument en vektor \(d = (d_1, d_2, \cdots , d_n)\), hvor \(d_n\) er en vægt.

  1. For det andet definerer vi afstand mellem dokumenter ved hjælp af euklidisk afstand. Afstanden mellem to dokumenter \(d\) og \(d'\) er:

\[ \text{distance}(d, d') = \sqrt{\sum_{i=1}^{N} (d_i - d'_i)^2} \]

Dette er den direkte linje-afstand mellem to punkter i rummet, generaliseret til \(N\) dimensioner.

  1. \(K\)-means forsøger at partitionere vores \(n\) dokumenter i \(k\) clusters, hvor \(k\) er et tal, vi vælger på forhånd. Målet er at minimere variationen inden for hver cluster. Matematisk formuleres dette som within-cluster sum of squares (WCSS):

\[ \text{WCSS} = \sum_{j=1}^{k} \sum_{d \in C_j} \lVert d - \mu_j \rVert^2 \]

\(C_j\) er \(j\)’ende cluster. \(\mu_j\) er centroiden (gennemsnittet) af alle dokumenter i cluster \(j\), bestemt som \(\mu_j = \frac{1}{|C_j|} \sum_{d \in C_j} d\). \(\lVert d - \mu_j \rVert^2\) er den kvadrerede euklidiske afstand mellem dokument \(d\) og centroiden. Intuitionen er at vi vil have dokumenter i samme cluster til at ligge tæt på hinanden.

Clustering af nyhedsartikler i et todimensionalt\(^*\) rum

Forestil jer, at vi har repræsenteret tre artikler baseret på deres ordfrekvenser for ordene “politik” og “sport”.

  • Artikel A har vektoren \((0.8, 0.1)\), artikel B har \((0.7, 0.2)\), og artikel C har \((0.1, 0.9)\).
  • Vi vil inddele dem i \(k = 2\) clusters.
  • Vi initialiserer ved at vælge artikel A og C som startcentroider, så \(\mu_1 = (0.8, 0.1)\) og \(\mu_2 = (0.1, 0.9)\)
  • Vi beregner herefter “afstande”: For artikel B finder vi afstanden til \(\mu_1: \sqrt{(0.7 - 0.8)^2 + (0.2 - 0.1)^2} = \sqrt{0.01 + 0.01} = 0.141\) mens afstanden til \(\mu_2: \sqrt{(0.7 - 0.1)^2 + (0.2 - 0.9)^2} = \sqrt{0.36 + 0.49} = 0.922\). Artikel B tildeles derfor cluster \(1\), da den er nærmest \(\mu_1\).
  • Vi fortsætter denne proces, indtil algoritmen har konvergeret ved at ingen dokumenter ville skifte cluster i næste iteration.
  • En vigtig begrænsning er, at \(k\)-means er følsom over for initialiseringen og kan konvergere til forskellige lokale optima afhængigt af startpunkterne. Derfor kører man typisk algoritmen flere gange med forskellige tilfældige initialiseringer og vælger den løsning med den laveste WCSS.

Valg af k: Elbow-metoden

Et centralt spørgsmål i \(k\)-means er, hvordan vi vælger antallet af clusters \(k\).

  • En udbredt tilgang er elbow-metoden, hvor vi kører \(k\)-means for forskellige værdier af \(k\) og plotter WCSS som funktion af \(k\).
  • WCSS vil altid falde, når k stiger, men vi leder efter det punkt, hvor tilføjelsen af flere clusters giver aftagende gevinster: “albuen” i kurven.
   WCSS (Within-Cluster Sum of Squares)
    |
1000|●
    |
 800| ●
    |
 600|  ●
    |   ╲
 400|    ●
    |     ╲
 200|       ●___●___●___●___●
    |
   0|_________________________________ k
     1   2   3   4   5   6   7   8   9

  Stejl fald    |  Fladt plateau
  (stor gevinst)|  (lille gevinst)
                ↑
          Optimal k ≈ 4

Altså, for at kunne clustre dokumenter skal vi forstå hvordan vi matematisk kan udtrykke hvor meget eller lidt to dokumenter ligner hinanden.

Hvert dokument bliver repræsenteret som en vektor i et højdimensionelt rum. Hvis vores corpus har \(3000\) forskellige ord, bliver hvert dokument en vektor med \(3000\) tal der viser hvor ofte hvert ord forekommer. For to dokumenter, \(f_i\) og \(f_j\), kan vi beregne forskellige distance measures.

  1. Euclidean distance (\(L2\)): Tænk på det som at måle den rette linje mellem to punkter i rummet. Hvis dokument A har ordet “demokrati” \(10\) gange og dokument B har det \(3\) gange, bidrager dette ord med \((10-3)^2 = 49\) til den samlede distance. Læg alle sådanne bidrag sammen for alle ord, og tag kvadratroden. \[ L_2(f_i, f_j) = \sum_{r=1}^{m} (f_{ir} - f_{jr})^2 \]

  2. Manhattan distance (\(L1\)): I stedet for at kvadrere forskellene, tager vi den absolutte værdi af hver forskel og lægger dem sammen. Det giver et andet billede af distance end fugleflugt-linjen (Euclidean).

  1. Cosine distance: måler vinklen mellem deres vektorer. To dokumenter der har præcis samme ordfordeling vil have en vinkel på \(0\) grader mellem sig (cosine distance \(= 0\)), selv hvis det ene dokument er meget længere end det andet.

\[ CD(f_i, f_j) = 1 - \frac{\sum_{r=1}^{m} f_{ir} f_{jr}}{\sqrt{\sum_{r=1}^{m} f_{ir}^2} \, \sqrt{\sum_{r=1}^{m} f_{jr}^2}} \]

\(k\)-means klyngeanalyse i UCloud

Forskelle

  • Naive Bayes lever i sandsynlighedsteorien verden: givet en kategori, hvad er sandsynligheden for at generere netop disse ord? Dette gør den til en probabilistisk model, hvor output altid er sandsynligheder, der kan fortolkes direkte.
    • Naive Bayes vil have problemer med stærkt korrelerede features og kan fejlberegne, hvis dens uafhængighedsantagelse er for groft overtrådt.
  • \(K\)-means derimod er deterministisk og geometrisk. Sandsynligheder spiller ikke en rolle, da det handler om afstande i et vektorrum. Dens mål er rent metrisk: minimer den samlede kvadrerede afstand mellem punkter og deres cluster-centre. Dette gør den intuitivt lettere at visualisere, men også mere følsom over for valget af afstandsmål.
    • \(K\)-means kan have “problemer” med overlappende clusters, outliers, eller clusters med vidt forskellige størrelser og former.
  • Begge (typer af) metoder transformerer tekst til numeriske repræsentationer, som computeren kan arbejde med. Denne transformation sker gennem teknikker som bag-of-words og TF-IDF vægtning.
  • Begge tilgange kræver betydelige mængder data for at fungere godt. Jo mere tekst vi fodrer metoderne med, desto bedre bliver de til at fange sprogets nuancer.

Udfordringerne i computationel tekstanalyse

Det skrevne sprog er ikke entydigt!

Jeg elsker politik

Jeg elsker ikke politik

Jeg elsker politik, når folk råber i munden på hinanden

Jeg elsker politik, når folk råber i munden på hinanden. Først da kan jeg mærke, hvor passionerede politikerne er.

Tim valgte kort før Allan.

  • Valgte Tim kort i et spil, før Allan valgte kort?
  • Valgte Tim et eller andet lige inden, at Allan valgte noget?
  • Valgte Tim kort inden han valgte Allan?

Dimensionalitetsreduktion

Tekstdata har altid høj dimensionalitet (mange variable)

  • Høj dimensionalitet gør det vanskeligere at opsummere data simpelt, og mange modeller har svært ved at håndtere det.
  • Høj dimensionalitet bevirker typisk også høj sparsity (mange \(0\)-tællinger), som også er en udfordring for mange modeller og metoder.
  • Ikke blot ordene i teksten, men også sætningskonstruktion (syntaks) og ordenes betydning (semantik) er nødvendige for at kunne opsummere teksten fyldestgørende
  • Derudover kan der være faktorer, som går ud over teksten i sig selv, der kan være relevante for, hvordan den skal forstås (historisk kontekst, forfatter, medie osv.)

Tekstdata er vanskelige at standardisere

  • Gradbøjning: vælge, vælger, valgte
  • Ens stavemåder: en vælger (navneord), han vælger (verbum)
  • Alternative stavemåder: ressource, resurse
  • Synonymer: stille, sagte
  • Forskellig semantisk “vægt”: hvor relevant er ordet for teksten?

Tekstdata har ikke en given datastruktur

  • Opgjort på ord, sætninger, afsnit?
  • Tællinger? Vægtning?
  • Sammenlignes tekster, ord, ordforbindelser, kontekst?

Repræsentation af tekstdata

Numerisk repræsentation af tekst

  • Ord og tekster kan repræsenteres numerisk på forskellig vis.
    • Teoretisk udfordring: Hvordan bevarer vi kontekst for et ord?
    • Teknisk udfordring: Hvordan reduceres dimensionalitet uden at miste information om ordets kontekst?
  • Vi anvender termet “text vectorization” til at referere til teknikker, der konverterer tekster til matematiske repræsentationer (datastrukturer, som man kan foretage beregninger på)
  • Datastrukturer for tekster ikke givet.
    • Datastruktur afhænger både af, hvordan tekst er konverteret til tokens samt af, hvordan ordene skal opgøres (tælles, vægtes eller andet).

Overvejelser

  1. Hvordan skal tekst bearbejdes? (standardisering, sortering, udvælgelse)
  2. Hvordan skal tekst repræsenteres? (datastruktur)
  3. Hvordan skal tekst analyseres? (anvendte teknikker, metoder, modeller) 2 og 3 hænger ofte sammen (repræsentation afhænger af analysen, som skal foretages).

vectorization: Bag-of-Words

Tekst 1: Katten spiser fisk

Tekst 2: Hunden spiser kød

Tekst 3: Katten og hunden leger

vocab = ['katten', 'spiser', 'fisk', 'hunden', 'kød', 'og', 'leger']

Bag-of-Words matrix:

Dokument katten spiser fisk hunden kød og leger
Tekst 1 1 1 1 0 0 0 0
Tekst 2 0 1 0 1 1 0 0
Tekst 3 1 0 0 1 0 1 1

vectorization: Document-term matrix

Tekst 1: Jeg elsker spam

Tekst 2: Jeg kan ikke fordrage spam

vocab = ['Jeg', 'elsker', 'spam', 'kan', 'ikke', 'fordrage']


tekst1 = [1, 1, 1, 0, 0, 0]
tekst2 = [1, 0, 1, 1, 1, 1]


import pandas as pd
df = pd.DataFrame([tekst1, tekst2], columns=vocab, index=['tekst1', 'tekst2'])
print(df)
        Jeg  elsker  spam  kan  ikke  fordrage
tekst1    1       1     1    0     0         0
tekst2    1       0     1    1     1         1
  • Én række per tekst, én kolonne per unikt ord/lemma/token (“types”).
  • Hver række er en vektor: Tal der afspejler sammenfaldende ord (co-occurrence).
  • Dimensionalitet svarende til antal types (“vocabulary”).
  • Høj sparsity (mange 0’er) er et problem for mange beregningsmetoder/modeller.

vectorization: term frequency-inverse document frequency

Bygger på en intuitiv idé om hvad der gør ord vigtige.

\[ \text{tf-idf} = \text{tf}(t, d) \times \text{idf}(t, D) \]

\(\text{tf}(t, d)\) er antal gange ord \(t\) er nævnt i dokument \(d\)

\(\text{idf}(t, D) = \log \left( \frac{D}{|\{ d \in D : t \in d \}|} \right)\) er logaritmen af det samlede antal dokumenter, \(D\), divideret med antallet af dokumenter, der indeholder termen

Med andre ord:

  • Vi udregner, hvor ofte ordet fremgår i en tekst set i forhold til, hvor mange tekster ordet fremgår i.
  • I udregningen vægtes ord, som fremgår i få tekster, op. Dette ud fra en antagelse om, at ord, der fremgår i få tekster, er mere sigende for indholdet af de tekster, som ordet indgår i.
  • idf “straffer” ord der optræder i mange dokumenter, fordi sådanne ord typisk ikke er særligt informative for at forstå hvad et specifikt dokument handler om.

Eksempel

Tekst 1: Katten spiser fisk og katten spiser mælk

Tekst 2: Hunden spiser kød og hunden løber hurtigt

Tekst 3: Katten leger og hunden leger sammen

Dokument 1: “Katten spiser fisk og katten spiser mælk” (8 ord total)

  • TF(katten) \(= 2/8 = 0.250\)
  • TF(spiser) \(= 2/8 = 0.250\)
  • TF(fisk) \(= 1/8 = 0.125\)
  • TF(og) \(= 1/8 = 0.125\)
  • TF(mælk) \(= 1/8 = 0.125\)

Dokument 2: “Hunden spiser kød og hunden løber hurtigt” (7 ord total)

  • TF(hunden) \(= 2/7 = 0.286\)
  • TF(spiser) \(= 1/7 = 0.143\)
  • TF(kød) \(= 1/7 = 0.143\)
  • TF(og) \(= 1/7 = 0.143\)
  • TF(løber) \(= 1/7 = 0.143\)
  • TF(hurtigt) =$ 1/7 = 0.143$

Dokument 3: “Katten leger og hunden leger sammen” (6 ord total)

  • TF(katten) \(= 1/6 = 0.167\)
  • TF(leger) \(= 2/6 = 0.333\)
  • TF(og) \(= 1/6 = 0.167\)
  • TF(hunden) \(= 1/6 = 0.167\)
  • TF(sammen) \(= 1/6 = 0.167\)

Totalt antal dokumenter: \(N = 3\)

  • IDF(katten) \(= log(3/2) = 0.176\)
  • IDF(spiser) \(= log(3/2) = 0.176\)
  • IDF(fisk) \(= log(3/1) = 0.477\)
  • IDF(og) = \(log(3/3) = 0.000\)
  • IDF(mælk) = \(log(3/1) = 0.477\)
  • IDF(hunden) \(= log(3/2) = 0.176\)
  • IDF(kød) = \(log(3/1) = 0.477\)
  • IDF(løber) \(= log(3/1) = 0.477\)
  • IDF(hurtigt) \(= log(3/1) = 0.477\)
  • IDF(leger) \(= log(3/1) = 0.477\)
  • IDF(sammen) \(= log(3/1) = 0.477\)

\[ \text{tf}(t, d) \times \text{idf}(t, D) \]

Dokument 1:

  • TF-IDF(katten) \(= 0.250 × 0.176 = 0.044\)
  • TF-IDF(spiser) \(= 0.250 × 0.176 = 0.044\)
  • TF-IDF(fisk) \(= 0.125 × 0.477 = 0.060\)
  • TF-IDF(og) \(= 0.125 × 0.000 = 0.000\)
  • TF-IDF(mælk) \(= 0.125 × 0.477 = 0.060\)

\[ \text{tf}(t, d) \times \text{idf}(t, D) \]

Dokument 2:

  • TF-IDF(hunden) \(= 0.286 × 0.176 = 0.050\)
  • TF-IDF(spiser) \(= 0.143 × 0.176 = 0.025\)
  • TF-IDF(kød) \(= 0.143 × 0.477 = 0.068\)
  • TF-IDF(og) \(= 0.143 × 0.000 = 0.000\)
  • TF-IDF(løber) \(= 0.143 × 0.477 = 0.068\)
  • TF-IDF(hurtigt) \(= 0.143 × 0.477 = 0.068\)

\[ \text{tf}(t, d) \times \text{idf}(t, D) \]

Dokument 3:

  • TF-IDF(katten) \(= 0.167 × 0.176 = 0.029\)
  • TF-IDF(leger) \(= 0.333 × 0.477 = 0.159\)
  • TF-IDF(og) \(= 0.167 × 0.000 = 0.000\)
  • TF-IDF(hunden) \(= 0.167 × 0.176 = 0.029\)
  • TF-IDF(sammen) \(= 0.167 × 0.477 = 0.080\)
Dokument katten spiser fisk og mælk hunden kød løber hurtigt leger sammen
Tekst 1 0.044 0.044 0.060 0.000 0.060 0 0 0 0 0 0
Tekst 2 0 0.025 0 0.000 0 0.050 0.068 0.068 0.068 0 0
Tekst 3 0.029 0 0 0.000 0 0.029 0 0 0 0.159 0.080
  • Ordet "og" optræder i alle tre dokumenter, hvilket giver det en IDF på præcis nul. Det betyder at uanset hvor mange gange “og” optræder i et dokument, så bliver dets TF-IDF værdi altid nul. Systemet har effektivt lært at ignorere dette almindelige bindeord, præcis som vi mennesker instinktivt gør når vi læser.
  • TF-IDF justerer for både hvor meget plads et ord optager i et dokument og hvor sjældent det er på tværs af hele samlingen. Den fanger den balance mellem lokal vigtighed og global unikhed, som vi intuitivt bruger når vi selv skal bestemme hvilke ord der definerer en teksts indhold.
  • Et ord kan optræde mange gange og stadig være uvæsentligt hvis det er for almindeligt, og et sjældent ord kan være mindre vigtigt hvis det kun nævnes en enkelt gang tilfældigt. TF-IDF finder de ord hvor både frekvens og sjældenhed peger i samme retning.

vectorization: ordtabel

Tekst 1: Vi er utroligt beærede

Tekst 2: Vi vil gerne dele prisen med alle

Tekst 3: Det skal vi have gjort op med.

ID Word POS Order doc:ID
0 Vi PRON 1 1
1 er AUX 2 1
2 utroligt ADV 3 1
3 beærede VERB 4 1
4 Vi PRON 1 2
5 vil AUX 2 2
6 gerne ADV 3 2
7 dele VERB 4 2
8 prisen NOUN 5 2
9 med ADP 6 2

vectorization: graf/netværk

ID From To
1 Vi er
1 er utroligt
1 utroligt beærede

ID’et (1) angiver at alle disse forbindelser tilhører den samme sætning.

Preprocessing-valg og konsekvenser

Pas på medUkritisk teknologioverførsel:

  • Preprocessing-råd kommer primært fra supervised learning (klassifikation) og anvendes ukritisk på unsupervised metoder (topic models, scaling)

  • … men disse to tilgange har forskellige mål og evalueringsmetrikker.

Trin Beskrivelse Potentielle problemer
P - Punctuation Fjern tegnsætning Kan være informativ i visse domæner (f.eks. hashtags)
N - Numbers Fjern tal “Section 423” kan være substantivt vigtig
L - Lowercasing Små bogstaver “Rose” (navn) vs “rose” (blomst)
S - Stemming Reduktion til ordstamme “partying” og “parties” → “parti”
W - Stopwords Fjern almindelige ord Ingen guldstandard-liste; 100–1000 ord
3 - n-grams Inkluder 2- og 3-grams “national defense” vs “national debt”
I - Infrequent Fjern sjældne termer Typisk <1% document frequency

Preprocessing

Tokens

Opdeling af tekst til enkelte tokens (ordenheder)

“Politiet har givet borgerne råd” -> [‘Politiet‘, ‘har‘, ‘givet‘, ‘borgerne‘, ‘råd‘]

  • Evt. frasortering af tegnsætning
  • Evt. konvertering til små bogstaver (lower-casing)

N-grams

Typisk referer tokenization til at opdele tekster i enkeltord, men afhængig af formål, kan det være relevant at se på tokens som “ordpar” (bigrams) eller “ordsamlinger” (n-grams).

’Politiet har givet borgerne råd‘ -> [(‘Politiet‘, ‘har‘), (‘har‘, ‘givet‘), (‘givet‘, ‘borgerne‘), (‘borgerne‘, ‘råd‘)]

Stemming

Typisk vil man ikke behandle ord med forskellig gradbøjning som forskellige ord. Simpleste måde at ensrette er at omdanne til ordstammen.

  • køre -> kør
  • kører -> kør
  • køren -> kør
  • kørte -> kørt

Der er flere algoritmer til at lave stemming—og de ikke altid enige!

  • Snowball algoritme (sprogfølsom): køren -> kør
  • Porter algoritme (ikke sprogfølsom): køren -> køren

Lemming

Konverterer ordet til grammtiske stamme (alternativt til stemming)

  • køre -> køre
  • kører -> køre
  • køren -> køren
  • kørte -> køre

Lemmatizere er af nødvendighed sprogafhængige

  • Findes typisk som “dictionary-modeller” (nogen har lavet opgørelser af, hvilke ord har samme grammatiske stamme)

Stopord

Mange ord indgår i tekst uanset hvad teksten handler om (bindeord, stedord, forholdsord o.l.)

  • Refereres til som “stopord” og frasorteres da de typisk ikke giver indblik i, hvad tekster handler om.
  • Stopord kan både være generelle (bindeord, stedord, forholdsord) eller kontekstspecifikke (fx ‘ordfører’ i referater fra Folketinget).

POS tagging

Opdeler ord i ordklasser: navneord, udsagnsord, tillægsord osv.

’Politiet har givet borgerne råd‘ -> ‘Politiet_NOUN har_AUX givet_VERB borgerne_NOUN råd_NOUN‘

  • Brugbart til at udlede meningsfulde ord i tekst (tillægsord siger typisk mere om teksten end forholdsord).
  • Er nødt til at være sprogafhængige og kontekstafhængige (fleste er baseret på maskinlæring i dag).

Dependency parsing

Dependency parsing udleder sætningskonstruktion: grundled, udsagnsled, genstandsled.

’Politiet har givet borgerne råd‘ -> ‘Politiet_nsubj har_aux givet_ROOT borgerne_obj råd_obj‘

  • Hjælper til at reducere tvetydighed i tekst: “hvem gjorde hvad til hvem?”
  • Brugbart til at udlede meningsfulde dele af teksten (fx fokusere på visse ord, der indgår som genstandsled i sætninger).
  • Er nødt til at være sprogafhængige og kontekstafhængige (fleste er baseret på maskinlæring i dag).

Sprogmodeller

Kort om det grundlæggende

En sprogmodel (“language model”) er kort sagt en model til at forudsige ord. Lidt forsimplet er en sprogmodel en model, som er trænet til at genkende ordtyper, entiteter og sætningskonstruktion, som derved kan prædiktere disse informationer i tekst (for det meste inden for samme sprog).

  • En sprogmodel er i sin essens en sandsynlighedsfordeling over sekvenser af ord. Den forsøger at besvare spørgsmålet: “Hvor sandsynlig er denne sekvens af ord i det sprog, jeg modellerer?”
  • Givet en sekvens af ord \(w_1, w_2, \cdots, w_n\), hvad er sandsynligheden \(P(w_1, w_2, \cdots, w_n)\)?

Forestil jer, at vi skal forudsige det næste ord i sætningen "Jeg går i...".

  • Vi har en intuitiv fornemmelse for, at "skole", "biografen" eller "parken" er sandsynlige fortsættelser, mens "grøn" eller "sov" er usandsynlige.
  • En sprogmodel formaliserer denne intuition matematisk ved at lære sandsynlighedsfordelinger fra store mængder tekst.

Næsten alle NLP-opgaver kan reduceres til eller drage fordel af sprogmodeller.

  • Part-of-speech tagging spørger: “Hvad er den mest sandsynlige ordklasse for dette ord i denne kontekst?”
  • Named entity recognition spørger: “Hvad er den mest sandsynlige entitetstype for denne ordsekvens?”
  • Maskinoversættelse kan ses som: “Hvad er den mest sandsynlige sætning i målsproget givet kildesætningen?”

Repræsentationsproblemet

Computere kan kun arbejde med tal, men sprog er symboler med komplekse relationer og betydninger.

velfærd:    [1, 0, 0, 0, 0, 0]
uddannelse: [0, 1, 0, 0, 0, 0]
sundhed:    [0, 0, 1, 0, 0, 0]
forsvar:    [0, 0, 0, 1, 0, 0]
regering:   [0, 0, 0, 0, 1, 0]
opposition: [0, 0, 0, 0, 0, 1]
  • Problem 1: Ingen semantisk information. Afstanden mellem "velfærd" og "uddannelse" er nøjagtig den samme som mellem "velfærd" og "forsvar". Computeren seks totalt forskellige ord uden nogen relation. Men intuitivt hører velfærd og uddannelse sammen i den sociale politiske sfære, mens forsvar er noget helt andet.

  • Problem 2: Dimensionalitetseksplosion. Et realistisk ordforråd har \(50,000-100,000\) ord. Det betyder vektorer med \(50,000-100,000\) dimensioner, hvor \(99.999\%\) er nuller. Ekstremt ineffektivt og gør beregninger umulige.

  • Problem 3: Ingen generalisering. Hvis modellen har lært at "regeringen øgede velfærdsudgifter" er positivt korreleret med socialdemokratisk politik, ved den intet om "administrationen forhøjede sociale udgifter", selvom sætningerne er semantisk identiske. Hvert ord behandles som totalt uafhængigt.

“You shall know a word by the company it keeps”: et ords betydning er defineret af de kontekster, det optræder i.

Vi har brug for er en repræsentation, der fanger, at ord der optræder i lignende kontekster har lignende betydninger: word embeddings.

Fra symboler til vektorer

Ideen bag word embeddings er at mappe hvert ord til et punkt i et kontinuerligt, høj-dimensionalt rum, hvor geometrisk afstand reflekterer semantisk lighed.

Et lille corpus om politik:

  1. “Regeringen forøgede velfærdsudgifterne”
  2. “Oppositionen kritiserede velfærdspolitikken”
  3. “Administrationen reducerede forsvarsbudgettet”
  4. “Regeringen styrkede forsvarsinvesteringer”
  5. “Oppositionen roste uddannelsesindsatsen”

Vi kan tælle hvilke ord der optræder sammen (inden for et vindue på \(±2\) ord).

Kontekst for “regeringen”:

  • Venstre: (intet, start af sætning)
  • Højre: forøgede, styrkede
  • Samlede naboer: {forøgede, styrkede, velfærdsudgifterne, forsvarsinvesteringer}

Kontekst for “oppositionen”:

  • Samlede naboer: {kritiserede, roste, velfærdspolitikken, uddannelsesindsatsen}

Kontekst for “administrationen”:

  • Samlede naboer: {reducerede, forsvarsbudgettet}

Repræsenteret som co-occurrence matrix. Hver række er et ord, hver kolonne er et kontekstord, og tallet er hvor mange gange de optræder sammen:

forøgede reducerede kritiserede styrkede roste velfærd* forsvar* uddannelse*
regeringen 1 0 0 1 0 1 1 0
oppositionen 0 0 1 0 1 1 0 1
administrationen 0 1 0 0 0 0 1 0

Men disse vektorer er stadig meget sparse (mange nuller) og høj-dimensionelle

Word embeddings (Skip-gram arkitekturen)

Skip-gram modellen siger: “Givet et ord, forudsig dets kontekst.”

Ideen er, at ord med lignende betydninger optræder i lignende kontekster. Hvis vi ofte ser “hund” i nærheden af “gøede”, “bold”, “gå tur”, vil vi sandsynligvis også se “kat” i lignende kontekster (måske “miavede”, “legetøj”, “kæle”).

Lad os forestille os, at efter træning på et stort politisk corpus har vi lært disse embeddings (forenklet til 2D):

dim1 (økonomisk venstre-højre) dim2 (institutionel magt)
velfærd -0.8 0.1
uddannelse -0.6 0.2
sundhed -0.7 0.3
forsvar 0.3 0.4
sikkerhed 0.4 0.5
regering 0.0 0.9
opposition 0.0 0.7
minister 0.1 0.8
borgmester -0.1 0.6
  • Dimension 1 fanger noget om politisk orientering: velfærd/uddannelse (venstre) vs. forsvar/sikkerhed (højre)
  • Dimension 2 fanger institutionel magt: regering/minister høj, almindelige politikområder lav

Fra disse dimensioner kan vi beregne semantisk lighed.

Cosinus-ligheden mellem to vektorer:

\[ \text{similarity}(\mathbf{v}_1, \mathbf{v}_2) = \frac{\mathbf{v}_1 \cdot \mathbf{v}_2}{\|\mathbf{v}_1\| \times \|\mathbf{v}_2\|} \]

Cosinus-lighed mellem “velfærd” og “uddannelse”

Vektorerne:

\[ \mathbf{v}_1 = [-0.8, 0.1] \quad \text{og} \quad \mathbf{v}_2 = [-0.6, 0.2] \]

Dot product:

\[ \mathbf{v}_1 \cdot \mathbf{v}_2 = (-0.8) \times (-0.6) + 0.1 \times 0.2 = 0.48 + 0.02 = 0.50 \]

Vektorlængder:

\[ \|\mathbf{v}_1\| = \sqrt{0.64 + 0.01} = \sqrt{0.65} \approx 0.806 \]

\[ \|\mathbf{v}_2\| = \sqrt{0.36 + 0.04} = \sqrt{0.40} \approx 0.632 \]

Cosinus-lighed:

\[ \text{similarity} = \frac{0.50}{0.806 \times 0.632} = \frac{0.50}{0.509} \approx 0.982 \]

Dette viser at “velfærd” og “uddannelse” har meget høj semantisk lighed (tæt på 1).

Cosinus-lighed mellem “velfærd” og “forsvar”

Vektorerne:

\[ \mathbf{v}_1 = [-0.8, 0.1] \quad \text{og} \quad \mathbf{v}_2 = [0.3, 0.4] \]

Dot product:

\[ \mathbf{v}_1 \cdot \mathbf{v}_2 = (-0.8) \times 0.3 + 0.1 \times 0.4 = -0.24 + 0.04 = -0.20 \]

Vektorlængder:

\[ \|\mathbf{v}_1\| \approx 0.806 \]

\[ \|\mathbf{v}_2\| = \sqrt{0.09 + 0.16} = \sqrt{0.25} = 0.5 \]

Cosinus-lighed:

\[ \text{similarity} = \frac{-0.20}{0.806 \times 0.5} = \frac{-0.20}{0.403} \approx -0.496 \]

Dette viser at “velfærd” og “forsvar” har negativ semantisk lighed (modsat rettede politiske koncepter).

Dimensionalitet og hvad dimensioner er i tekster

y
│
0.4  •forsvar •skat
│
0.3                 •uddannelse
│
0.2                    •velfærd
│
└──────────────────────────────── x
    -0.6   -0.5       0.7   0.8
  • X-aksen (dimension 1): Måske politisk orientering (venstre/højre)
  • Y-aksen (dimension 2): Hvad kunne det være?

Hvad betyder det så hvis vi har 300 dimensioner?

ord_vector = [
    0.32,   # dimension 1
    -0.18,  # dimension 2
    0.91,   # dimension 3
    -0.45,  # dimension 4
    0.72,   # dimension 5
    ...     # ... 295 flere dimensioner
    -0.23   # dimension 300
]

Dimensionerne er ikke foruddefinerede som “politisk orientering” eller “sport”. De emergerer under træningen. Modellen opdager selv, hvilke akser der er nyttige til at skelne ord fra hinanden.

En hypotetisk dimension

Dimension X, værdier:
king      :  0.823
queen     : -0.791
man       :  0.756
woman     : -0.742
boy       :  0.612
girl      : -0.598
prince    :  0.834
princess  : -0.808
father    :  0.701
mother    : -0.688

Mønster: Dimension kunne se ud til at fange køn. Positive værdier = maskulin, negative = feminin.

En anden hypotetisk dimension:

Dimension Y værdier:
king      :  0.891
queen     :  0.876
prince    :  0.723
princess  :  0.712
peasant   : -0.634
servant   : -0.612

Mønster: Dimensionen fanger måske social status eller royal status.

Men de fleste dimensioner er ikke fortolkelige!

  • Størstedelen af dimensioner er ikke direkte fortolkelige for mennesker. De repræsenterer komplekse, ikke-lineære kombinationer af semantiske features.
  • Hvorfor 300 dimensioner?
    • \(<100\) dimensioner: For lidt kapacitet til at fange alle nuancer
    • \(300-500\) dimensioner: god præcision, rimelig effektivitet
    • \(>1000\) dimensioner: Marginale forbedringer, men meget dyrere
"da_core_news_sm": "Ingen vectors"
"da_core_news_md": "96 dimensioner"
"da_core_news_lg": "300 dimensioner"
"BERT":            "768 dimensioner"
"GPT-3":           "12,288 dimensioner"

Dimensionalitetsreduktion: Fra 300 til 2

Ofte vil vi visualisere embeddings, men vi kan ikke visualisere 300 dimensioner. Så reducerer vi til 2 (eller 3) dimensioner.

  • PCA finder de 2 retninger i det 300-dimensionale rum, hvor dataene varierer mest.
import spacy
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np

# python3 -m spacy download da_core_news_md
nlp = spacy.load("da_core_news_md")

# Hent vektorer for politiske ord
words = ["velfærd", "uddannelse", "sundhed", "forsvar", "politi", 
         "skat", "regering", "opposition"]

vectors = []
for word in words:
    doc = nlp(word)
    if doc[0].has_vector:
        vectors.append(doc[0].vector)

vectors = np.array(vectors)  # Shape: [8 words × 300 dimensions]

# Reducer til 2 dimensioner
pca = PCA(n_components=2)
vectors_2d = pca.fit_transform(vectors)  # Shape: [8 words × 2 dimensions]
# Plot
plt.figure(figsize=(10, 8))
for i, word in enumerate(words):
    x, y = vectors_2d[i]
    plt.scatter(x, y)
    plt.annotate(word, (x, y), fontsize=12)
plt.xlabel('PC1')
plt.ylabel('PC2')
plt.title('Politiske ord i 2D rum (PCA)')
plt.show()

Eksempel: Analyser kønsassociationer med karrierer

  • Brug word embeddings (ordvektorer) til at undersøge implicitte kønsassociationer i sproget.
  • Måler hvor meget en karriere-vektor “peger” i samme retning som køns-aksen
male_words = nlp("han mand far dreng")
male = np.mean([token.vector for token in male_words], axis=0)

female_words = nlp("hun kvinde mor pige")
female = np.mean([token.vector for token in female_words], axis=0)

gender_axis = male - female

# Karrierer at teste
careers = ["læge", "sygeplejerske", "ingeniør", "lærer", 
           "direktør", "sekretær", "pilot", "stewardesse"]

career_gender_scores = {}
for career in careers:
    career_vector = nlp(career)[0].vector
    # Projicér på køns-akse (positiv = mandlig association)
    score = np.dot(career_vector, gender_axis)
    career_gender_scores[career] = score
print("Kønsassociationer (positiv = mandlig, negativ = kvindelig):")
for career, score in sorted(career_gender_scores.items(), key=lambda x: x[1], reverse=True):
    print(f"{career:15s}: {score:8.2f}")
Kønsassociationer (positiv = mandlig, negativ = kvindelig):
direktør       :   121.98
ingeniør       :   102.63
pilot          :    63.46
stewardesse    :    63.46
sekretær       :    14.40
lærer          :   -13.28
læge           :   -38.33
sygeplejerske  :   -78.60

Geometrisk fortolkning:

  • Word embeddings placerer ord i et 300-dimensionelt rum

  • Ord der bruges i lignende kontekster ligger tæt på hinanden

  • Køns-aksen er en linje gennem dette rum der går fra “kvindelige” ord til “mandlige” ord

  • Score = hvor langt en karriere projiceres langs denne akse

Sproglig fortolkning:

  • Scoren afspejler hvor ofte karrieren optræder sammen med kønnede ord i træningsdataene:

    • Høj positiv score → ofte nævnt sammen med “han”, “mand”, mandlige navne, etc.
    • Høj negativ score → ofte nævnt sammen med “hun”, “kvinde”, kvindelige navne, etc.
ingeniør        :   152.78
direktør        :   143.77
  • Disse ord optræder meget hyppigt i mandlige kontekster i træningsdataene
  • Eksempler kunne være: “Han er ingeniør”, “direktøren og hans team”
  • Afspejler både historisk realitet (flere mænd i disse roller) og sproglige konventioner
pilot           :    89.93
stewardesse     :    89.93  ← INTERESSANT!?
  • Pilot giver mening: en historisk mandsdomineret profession.
  • Men hvorfor har “stewardesse” en mandlig score?!
  • Mulige forklaringer:
    • Ordet bruges ofte i sammenhæng med “pilot”:
      • “Piloten og stewardessen” → “pilot” trækker det mod mandlig
    • Moderne sprogbrug bruger “flyvemedarbejder” i stedet:
      • “Stewardesse” kan være et ældre/mindre brugt ord i træningsdataene
    • Lille datasæt problem:
      • Hvis ordet forekommer sjældent, bliver embeddings upålidelige

Hvor ser vi denne bias i praksis?

  1. Jobopslag-screening AI:
    • Kan favorisere mænd til ingeniørjob og kvinder til sygeplejejob
  2. CV-parsing systemer:
    • Kan misfortolke køn baseret på karriere-nævnelser
  3. Søgeresultater:
    • “Succesful CEO” giver billeder af mænd
    • “Omsorgsfuld sygeplejerske” giver billeder af kvinder

Dette afslører bias i træningsdataene, ikke en sandhed …

  • Modellen er trænet på tekst (fx internettet, bøger, artikler)
  • Den lærer associationer der findes i sproget
  • Positive/negative scores afspejler samfundets stereotype opfattelser, ikke faktiske evner eller egnethed
  • Sprogmodeller kan indeholde samfundsmæssige fordomme.
  • Modellerne er et spejl af de tekster modellen er trænet på.

SpaCy

spaCy indeholder forskellige sprogmodeller—herunder en dansk sprogmodel. SpaCy’s sprogmodeller indeholder blandt andet:

  • Tokenizer (inddeling i enkeltord)
  • Lemmatizer (konvertering til navneform)
  • Part-Of-Speech tagging (POS-tagging) (identificering af ordtyper)
  • Dependency parsing (sætningskonstruktion)
  • Named-Entity-Recognition (NER) (udledning af “named entities”, fx personer og organisationer)

SpaCy’s større modeller (medium og large) kommer med pretrained word vectors. Disse er typisk trænet på enorme korpera (milliarder af tokens) ved hjælp af algoritmer som Word2Vec, GloVe eller FastText.

import spacy
import numpy as np

# Load en model med word vectors
# python3 -m spacy download en_core_web_md
nlp = spacy.load("en_core_web_md")

# Få vektoren for et ord
doc = nlp("dog cat astronaut")
dog_vector = doc[0].vector  # En numpy array af dimensionalitet 300

print(f"Vektordimension: {len(dog_vector)}")
print(f"All komponenter: {dog_vector}")
Vektordimension: 300
All komponenter: [-0.72483    0.42538    0.025489  -0.39807    0.037463  -0.29811
 -0.28279    0.29333    0.57775    1.2205    -0.27903    0.80879
 -0.71291    0.045808  -0.46751    0.55944    0.42745    0.58238
  0.20854   -0.42718   -0.40284   -0.048941   0.1149    -0.6963
 -0.03338    0.052596  -0.22572   -0.35996    0.47961   -0.38386
 -0.73837    0.1718     0.52188    0.45584   -0.026621   0.48831
  0.67996   -0.73345   -0.27078    0.41739    0.1947     0.27389
 -0.70931   -0.45317   -0.22574   -0.12617    0.03268    0.142
  0.53923   -0.61285   -0.5322     0.19479    0.13889   -0.020284
  0.088162   0.85337    0.039407   0.11529   -0.42646    0.74832
  0.34421   -0.59462    0.0040537  0.027203  -0.063394   0.26538
  0.34757    0.21395   -0.39799   -0.027067  -0.36132    0.31979
  0.55813   -0.5652     0.55382    0.03928   -0.26933   -0.14705
  0.74032   -0.50566    0.023765   0.62273   -0.79388   -0.25165
  0.11992   -0.43056    1.0614     0.58571    0.8856    -0.056054
  0.055826   0.30485    0.64639   -0.43831   -0.45706    0.036471
 -0.3466    -0.56219    0.28105   -0.33758   -0.041398   0.22171
  0.05262    0.18113    0.65646   -0.56217    0.038915  -0.30335
  0.05051   -0.2354     0.3233     0.31744    0.52453   -0.47154
  0.13152   -0.15104    0.14265   -0.20747    0.060413  -0.030342
 -0.092883   0.80421   -0.12497   -0.56199    0.29128   -0.22488
  0.30282   -0.0045144 -0.12305    0.20396   -0.32202   -0.11409
 -0.37613    0.40457    0.21461    0.25741   -0.36489    0.94135
  0.42725    0.022925  -1.8699    -0.76035    0.73771    0.36998
  0.50214   -0.30617   -0.26526    0.86573    0.3808     0.14754
  0.29932   -0.078863  -0.28992   -0.064636  -0.68914    0.19527
 -0.56368    0.26251   -0.52171   -1.0703     0.42478   -0.0067289
 -0.28591   -0.77831    0.049342   0.66675   -0.077419  -0.19226
  0.12721   -0.18844    0.13647    0.38804    0.21917   -0.24192
 -0.13465    0.23119   -0.43197    0.48302    0.3598     1.128
  0.019894  -0.10861   -0.13515   -0.34137   -0.36379    0.080616
  0.28682   -0.045819  -0.12114   -0.44835   -0.054611  -0.10362
  0.010954  -0.60063   -0.46665    0.15115   -0.31815   -0.58903
  1.1325     0.04406   -0.92863    0.3399    -0.03463   -0.40474
  0.17245   -0.19983   -0.095982  -0.074758   0.57472    0.25455
 -0.20387    0.055758  -0.65017    0.72629   -0.51083    0.11196
  0.44724    0.16157   -0.34571    0.19227   -0.063871   0.0057351
  0.48703   -0.53762   -0.73398   -0.11488    0.073723   0.58191
  0.33192   -0.13303   -0.3478    -0.022676  -0.32494   -0.26496
  0.56275    0.098558  -0.16671   -0.40481    0.55477   -0.58692
 -0.60433   -0.4227    -0.53712    0.2994     0.11339   -0.3154
 -0.28685    0.43999    0.013623   0.011139  -0.47734   -0.01492
  0.52524    0.53583    0.36626    0.23119   -0.1386     0.35374
 -0.27448    0.066183   0.6224    -0.24851   -0.36066    0.009084
 -0.58148    0.24371    0.29944   -0.025314  -0.73222    0.33236
 -0.40339    0.82624    0.006984   0.26737   -0.27695   -0.09713
 -0.015736   0.1024    -0.026831  -0.26293    0.31401    0.01051
 -0.048451  -0.74571    0.75827    0.67771    0.054738  -0.23325
  0.17996   -0.206      0.019095  -0.34283   -0.58602    0.0095634
 -0.085052   0.83312    0.31978    0.050317  -0.23159    0.28165  ]
# Beregn semantisk lighed
dog = nlp("dog")
cat = nlp("cat")
astronaut = nlp("astronaut")

print(f"dog <-> cat: {dog.similarity(cat)}")  # Høj (højt er typisk større end ~0.8)
print(f"dog <-> astronaut: {dog.similarity(astronaut)}")  # Lav (lavt er typisk mindre end ~0.2)
dog <-> cat: 1.0000001192092896
dog <-> astronaut: 0.12654462456703186
  • Similarity-funktionen bruger cosinus-afstanden mellem vektor-repræsentationer.
  • For dokumenter beregnes vektoren som gennemsnittet af alle token-vektorer, hvilket giver et overordnet semantisk fingeraftryk.
  • Nyttigt til metoder som dokument-clustering eller semantisk søgning.

Pipeline-arkitektur

Når vi arbejder med SpaCy, arbejder du fundamentalt med en pipeline.

  • Et spaCy pipeline tager én tekst ind og giver et doc-objekt tilbage
  • NLP-komponenter kan tilføjes og fjernes (obs på afhængigheder)
  • spaCy pipelines er tilgængelige gennem eksisterende sprogmodeller

Når modellen anvendes på et stykke tekst, kan de forskellige værdier og information, som er udledt af teksten, tilgås som attributes (et attribute for token, et for lemma, et for part-of-speech-tag osv.).

Forestil jer en tekststreng, der bevæger sig gennem en række behandlingsstadier, hvor hvert stadie tilføjer et lag af lingvistisk forståelse.

Pipeline-arkitekturen starter med råtekst og sender den gennem en række komponenter i en fastlagt rækkefølge.

  1. Første stop er tokenization, hvor teksten bliver opdelt i individuelle tokens—ord, tegnsætning og andre meningsfulde enheder. SpaCy’s tokenizer bruger en kombination af regelbaserede heuristikker og undtagelseslister til at navigere forviklinger: er “Dr. Hansen” to tokens og ikke tre? Hvordan håndteres “don’t”, er det én token eller to?
  2. Efter tokenization kommer part-of-speech tagging, hvor hvert token får tildelt en ordklasse. Er “flies” et substantiv eller et verbum? Konteksten afgør det, og SpaCy bruger statistiske modeller trænet på annoterede korpera til at træffe disse beslutninger. Disse modeller er typisk baseret på neurale netværk, der ser på omkringliggende tokens for at forudsige den mest sandsynlige tag.
  3. Næste i rækken er dependency parsing, som afdækker den syntaktiske struktur i sætninger. Her identificerer SpaCy, hvordan ord relaterer til hinanden: hvilket ord er subjektet, hvilket er objektet, hvilke ord modificerer andre ord. Dette repræsenteres som en træstruktur, hvor hvert ord har en grammatisk relation til et andet ord (dets “head”).
  4. Named entity recognition kommer derefter og identificerer navngivne entiteter i teksten – personer, organisationer, lokationer, datoer, og så videre. Dette er kritisk for mange anvendelser, fra at uddrage information fra nyhedsartikler til at anonymisere følsomme data.

Hele denne pipeline er modulariseret og konfigurbar. Du kan tilføje dine egne komponenter, fjerne dem du ikke har brug for, eller ændre rækkefølgen.

Datastrukturerne: Doc, Token og Span

  • Doc-objektet er SpaCy’s fundamentale enhed. Når vi sender en tekst gennem SpaCy’s pipeline, får vi et Doc-objekt tilbage. Dette er en sekvens af tokens med al den lingvistiske information, pipelinenen har tilføjet. (Det er smart fordi det er hukommelseseffektivt…)
    • Vi kan spørge en token om dets originalform, dets lemma (grundform), dets ordklasse, dets syntaktiske rolle, om det er en del af en navngiven entitet, osv.
  • Span-objekter repræsenterer sammenhængende sekvenser af tokens. En sætning er en span, en navngiven entitet er en span, en frase er en span.

Sprogmodellerne

Sprogmodellerne er ikke inkluderet i basisinstallationen, men downloades separat. Dette designvalg handler om at modeller kan være hundredvis af megabytes store, og forskellige brugere har brug for forskellige sprog og præcisionsniveauer.

https://spacy.io/models

python3 -m spacy download en_core_web_md

SpaCy tilbyder typisk modeller i tre størrelser for hvert sprog.

Når SpaCy er installeret, starter vi med at importere biblioteket og indlæse vores sprogmodel. Koden ser således ud:

import spacy

# python3 -m spacy download da_core_news_sm
# Indlæs den lille danske sprogmodel
nlp = spacy.load("da_core_news_sm")

Dette nlp-objekt er nu vores pipeline.

“Bag scenen” har SpaCy nu initialiseret alle pipeline-komponenter, indlæst de neurale netværk, og er klar til at behandle tekst. Objektet hedder traditionelt nlp, en konvention der gør kode på tværs af projekter let at læse.

spacy.load()

da_core_news_sm/
├── config.cfg                 # Konfiguration
├── meta.json                  # Metadata 
├── tokenizer                  # Tokenization regler
│   └── exceptions.json        
├── vocab/
│   ├── strings.json           # Ord → ID mapping 
│   ├── vectors                # Word embeddings 
│   │   └── vectors.bin        # 
│   └── lexemes.bin            # Ordfunktioner 
├── tagger/
│   ├── model                  # POS tagger vægte
│   │   └── model.bin          
│   └── cfg                    # Hyperparametre 
├── parser/
│   ├── model                  # Dependency parser vægte
│   │   └── model.bin          
│   └── cfg
├── ner/
│   ├── model                  # NER vægte
│   │   └── model.bin          
│   └── cfg
└── lemmatizer/
    └── lookups.bin            # Lemmatization tabel 

Direkte ID-opslag i SpaCy

import spacy

nlp = spacy.load("da_core_news_sm")

# Din tekst
tekst = "Regeringen øgede velfærdsudgifterne betydeligt."
doc = nlp(tekst)

# For hvert token kan du se dets ID
for token in doc:
    print(f"Ord: '{token.text}'")
    print(f"  Hash ID: {token.orth}")  # Det faktiske ID SpaCy bruger
    print(f"  Vocab entry: {nlp.vocab.strings[token.orth]}")
    print(f"  Lemma ID: {token.lemma}")
    print(f"  POS ID: {token.pos}")
    print()
Ord: 'Regeringen'
  Hash ID: 3849404314323585877
  Vocab entry: Regeringen
  Lemma ID: 17039087236279372374
  POS ID: 92

Ord: 'øgede'
  Hash ID: 1409674694569110436
  Vocab entry: øgede
  Lemma ID: 1540739943586306153
  POS ID: 100

Ord: 'velfærdsudgifterne'
  Hash ID: 14684542281918508293
  Vocab entry: velfærdsudgifterne
  Lemma ID: 8400297438331790641
  POS ID: 92

Ord: 'betydeligt'
  Hash ID: 12616862253303509379
  Vocab entry: betydeligt
  Lemma ID: 12616862253303509379
  POS ID: 86

Ord: '.'
  Hash ID: 12646065887601541794
  Vocab entry: .
  Lemma ID: 12646065887601541794
  POS ID: 97

Processering af tekst

# Processer en tekst gennem pipeline'n
doc = nlp("Mette Frederiksen forklarede beslutningen til journalisterne.")

# Undersøg tokenization
for token in doc:
    print(f"Token: {token.text}, Lemma: {token.lemma_}, POS: {token.pos_}")
Token: Mette, Lemma: Mette, POS: PROPN
Token: Frederiksen, Lemma: Frederiksen, POS: PROPN
Token: forklarede, Lemma: forklare, POS: VERB
Token: beslutningen, Lemma: beslutning, POS: NOUN
Token: til, Lemma: til, POS: ADP
Token: journalisterne, Lemma: journalist, POS: NOUN
Token: ., Lemma: ., POS: PUNCT
  • For hvert token får vi originalformen, lemmaet (grundformen), og ordklassen. “besøgte” får lemmaet “besøge”, og POS-tagget VERB, osv …

  • Bemærk at attributter med en underscore-endelse returnerer strenge, mens versioner uden underscore returnerer ID’er.

dependency parsing: Syntaktiske relationer

# Undersøg dependency-strukturen
for token in doc:
    print(f"{token.text} <- {token.dep_} <- {token.head.text}")
Mette <- nsubj <- forklarede
Frederiksen <- flat <- Mette
forklarede <- ROOT <- forklarede
beslutningen <- obj <- forklarede
til <- case <- journalisterne
journalisterne <- obl <- forklarede
. <- punct <- forklarede
  • nsubj: “Mette” er nominal subjekt til “forklarede” (hvem udfører handlingen)
  • flat: “Frederiksen” er flad relation til “Mette” (navn-konstruktion)
  • ROOT: “forklarede” er sætningens rod (hovedverbet)
  • obj: “beslutningen” er direkte objekt for “forklarede” (hvad blev forklaret)
  • case: “til” er præposition der markerer relationen til “journalisterne”
  • obl: “journalisterne” er oblique argument til “forklarede” (til hvem)
  • punct: “.” er punktum

Named entity recognition

# Find navngivne entiteter
for ent in doc.ents:
    print(f"Entitet: {ent.text}, Type: {ent.label_}")
Entitet: Mette Frederiksen, Type: PER

SpaCy vil identificere “Mette Frederiksen” som en person, men kan også genkende organisationer.

tekst = """
Statsminister Mette Frederiksen mødtes i går med EU-kommissionen i Bruxelles.
Danske Bank og Novo Nordisk annoncerede et samarbejde om grøn finansiering.
Udenrigsminister Lars Løkke Rasmussen kommenterede, at Danmark vil investere
25 milliarder kroner i klimaprojekter. Folketinget stemte med 90 stemmer for forslaget.
"""

doc = nlp(tekst)

# Udtræk organisationer og personer
organisationer = [ent.text for ent in doc.ents if ent.label_ == "ORG"]
personer = [ent.text for ent in doc.ents if ent.label_ == "PER"]
lokationer = [ent.text for ent in doc.ents if ent.label_ == "LOC"]

print("\n")
print(f"Organisationer fundet: {organisationer}")
print(f"Personer fundet: {personer}")
print(f"Lokationer fundet: {lokationer}")

# Find alle tal med deres kontekst
for token in doc:
    if token.like_num:
        kontekst = doc[max(0, token.i-3):min(len(doc), token.i+4)]
        print(f"Tal: {token.text}, Kontekst: {kontekst.text}")


Organisationer fundet: ['Danske Bank', 'Novo Nordisk', 'Folketinget']
Personer fundet: ['Mette Frederiksen', 'Lars Løkke Rasmussen']
Lokationer fundet: ['Bruxelles', 'Danmark']
Tal: et, Kontekst: Novo Nordisk annoncerede et samarbejde om grøn
Tal: 25, Kontekst: vil investere
25 milliarder kroner i
Tal: 90, Kontekst: Folketinget stemte med 90 stemmer for forslaget

Dette eks. fremhæver SpaCy’s “evne” til mønstergenkendelse og informationsudtræk.

SpaCy live